import datetime as dt
import html
import math
import os
from statistics import mean


class HandFlopReportGenerator:
    def __init__(self, trial_file_name):
        root, _ = os.path.splitext(trial_file_name)
        self.report_path = f"{root}_report.html"
        self.chart_image_path = f"{root}_report_chart.png"

    def generate_report(self, trial_data_dict, trial_info_dict):
        rows = self._build_event_rows(trial_data_dict)
        summary = self._build_summary(trial_data_dict, trial_info_dict, rows)
        chart_svg = self._build_chart_svg(rows)
        html_text = self._render_html(summary, rows, chart_svg)

        with open(self.report_path, "w", encoding="utf-8") as report_file:
            report_file.write(html_text)

        try:
            self._save_chart_image(rows)
        except Exception:
            # Preserve the HTML report even when PNG generation is unavailable.
            pass

        return self.report_path

    def _build_event_rows(self, trial_data_dict):
        reaction_times = trial_data_dict.get("reaction_time", [])
        times = trial_data_dict.get("time", [])
        targets = trial_data_dict.get("target_direction", [])
        target_angles = trial_data_dict.get("target_angle_deg", [])
        measured_angles = trial_data_dict.get("current_angle_deg", [])
        successes = trial_data_dict.get("correct_this_interval", [])

        rows = []
        for index, reaction_time in enumerate(reaction_times):
            if float(reaction_time) <= 0:
                continue

            target_label = self._target_label(targets[index] if index < len(targets) else 0)
            rows.append(
                {
                    "cue_number": len(rows) + 1,
                    "time": float(times[index]) if index < len(times) else 0.0,
                    "target_label": target_label,
                    "target_angle": float(target_angles[index]) if index < len(target_angles) else 0.0,
                    "measured_angle": float(measured_angles[index]) if index < len(measured_angles) else 0.0,
                    "reaction_time": float(reaction_time),
                    "outcome": "Success"
                    if int(successes[index]) == 1
                    else "Timed Out",
                }
            )

        return rows

    def _build_summary(self, trial_data_dict, trial_info_dict, rows):
        successful_rows = [row for row in rows if row["outcome"] == "Success"]
        reaction_times = [row["reaction_time"] for row in rows]
        success_times = [row["reaction_time"] for row in successful_rows]
        direction_comparison = self._build_direction_comparison(rows)
        start_time = str(self._get_value(trial_info_dict, "local_time", ""))
        formatted_start = self._format_start_time(start_time)
        elapsed_seconds = float(trial_data_dict.get("time", [0])[-1]) if trial_data_dict.get("time") else 0.0

        return {
            "app_name": self._get_value(trial_info_dict, "app_name", "Hand Flop WCB Game"),
            "trial_name": self._get_value(trial_info_dict, "trial_name", "Hand Flop Trial"),
            "participant_name": self._get_value(trial_info_dict, "participant_name", ""),
            "participant_institution": self._get_value(
                trial_info_dict, "participant_institution", ""
            ),
            "participant_email": self._get_value(trial_info_dict, "participant_email", ""),
            "start_time": formatted_start,
            "elapsed_seconds": elapsed_seconds,
            "average_reaction_time": float(trial_data_dict.get("running_score", [0])[-1])
            if trial_data_dict.get("running_score")
            else 0.0,
            "successful_cues": len(successful_rows),
            "timed_out_cues": len([row for row in rows if row["outcome"] == "Timed Out"]),
            "completed_cues": len(rows),
            "best_reaction_time": min(success_times) if success_times else 0.0,
            "worst_reaction_time": max(reaction_times) if reaction_times else 0.0,
            "average_success_time": mean(success_times) if success_times else 0.0,
            "direction_comparison": direction_comparison,
        }

    def _build_direction_comparison(self, rows):
        inside_rows = sorted(
            [row for row in rows if row["target_label"] == "inside_wrist"],
            key=lambda row: row["cue_number"],
        )
        outside_rows = sorted(
            [row for row in rows if row["target_label"] == "outside_wrist"],
            key=lambda row: row["cue_number"],
        )

        inside_times = [row["reaction_time"] for row in inside_rows]
        outside_times = [row["reaction_time"] for row in outside_rows]
        inside_mean = mean(inside_times) if inside_times else 0.0
        outside_mean = mean(outside_times) if outside_times else 0.0
        if len(inside_times) < 2 or len(outside_times) < 2:
            return {
                "available": False,
                "inside_count": len(inside_rows),
                "outside_count": len(outside_rows),
                "inside_mean": inside_mean,
                "outside_mean": outside_mean,
                "mean_difference": inside_mean - outside_mean,
                "t_statistic": 0.0,
                "p_value": 1.0,
            }

        try:
            from scipy import stats
        except Exception:
            return {
                "available": False,
                "inside_count": len(inside_rows),
                "outside_count": len(outside_rows),
                "inside_mean": inside_mean,
                "outside_mean": outside_mean,
                "mean_difference": inside_mean - outside_mean,
                "t_statistic": 0.0,
                "p_value": 1.0,
            }

        try:
            test_result = stats.ttest_ind(inside_times, outside_times, equal_var=False)
            t_statistic = float(test_result.statistic)
            p_value = float(test_result.pvalue)
        except Exception:
            return {
                "available": False,
                "inside_count": len(inside_rows),
                "outside_count": len(outside_rows),
                "inside_mean": inside_mean,
                "outside_mean": outside_mean,
                "mean_difference": inside_mean - outside_mean,
                "t_statistic": 0.0,
                "p_value": 1.0,
            }

        return {
            "available": True,
            "inside_count": len(inside_rows),
            "outside_count": len(outside_rows),
            "inside_mean": inside_mean,
            "outside_mean": outside_mean,
            "mean_difference": inside_mean - outside_mean,
            "t_statistic": t_statistic,
            "p_value": p_value,
            "significance_verdict": "statistically significant"
            if p_value < 0.05
            else "not statistically significant",
        }

    def _render_html(self, summary, rows, chart_svg):
        participant_name = html.escape(summary["participant_name"] or "Unspecified")
        participant_institution = html.escape(
            summary["participant_institution"] or "Unspecified"
        )
        participant_email = html.escape(summary["participant_email"] or "Unspecified")
        direction_comparison = summary["direction_comparison"]
        if direction_comparison["available"]:
            direction_comparison_html = f"""
        <div class="grid">
          <div class="metric"><div class="metric-label">Inside Samples</div><div class="metric-value">{direction_comparison['inside_count']}</div></div>
          <div class="metric"><div class="metric-label">Outside Samples</div><div class="metric-value">{direction_comparison['outside_count']}</div></div>
          <div class="metric"><div class="metric-label">Inside Avg RT</div><div class="metric-value">{direction_comparison['inside_mean']:.2f}s</div></div>
          <div class="metric"><div class="metric-label">Outside Avg RT</div><div class="metric-value">{direction_comparison['outside_mean']:.2f}s</div></div>
          <div class="metric"><div class="metric-label">Mean Difference</div><div class="metric-value">{direction_comparison['mean_difference']:+.2f}s</div></div>
          <div class="metric"><div class="metric-label">Welch t Statistic</div><div class="metric-value">{direction_comparison['t_statistic']:.2f}</div></div>
          <div class="metric"><div class="metric-label">Welch p Value</div><div class="metric-value">{direction_comparison['p_value']:.4f}</div></div>
          <div class="metric"><div class="metric-label">Verdict (p < 0.05)</div><div class="metric-value">{direction_comparison['significance_verdict']}</div></div>
        </div>
"""
        else:
            direction_comparison_html = f"""
        <p class="subtle">Inside samples: {direction_comparison['inside_count']} | Outside samples: {direction_comparison['outside_count']} | Welch t-test unavailable, so no significance verdict is reported.</p>
"""
        rows_html = "\n".join(self._render_row(row) for row in rows) or (
            "<tr><td colspan='6'>No completed cues were recorded.</td></tr>"
        )

        return f"""<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{html.escape(summary['trial_name'])} Report</title>
    <style>
      body {{
        font-family: Helvetica, Arial, sans-serif;
        margin: 0;
        background: #f4f6f8;
        color: #1f2933;
      }}
      main {{
        max-width: 980px;
        margin: 0 auto;
        padding: 32px 20px 48px;
      }}
      h1, h2 {{
        margin: 0 0 16px;
      }}
      .card {{
        background: #ffffff;
        border-radius: 14px;
        padding: 24px;
        margin-bottom: 24px;
        box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
      }}
      .grid {{
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
        gap: 16px;
      }}
      .metric {{
        background: #f8fafc;
        border-radius: 12px;
        padding: 16px;
      }}
      .metric-label {{
        font-size: 12px;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        color: #52606d;
      }}
      .metric-value {{
        font-size: 28px;
        margin-top: 8px;
        font-weight: 700;
      }}
      table {{
        width: 100%;
        border-collapse: collapse;
      }}
      th, td {{
        padding: 10px 12px;
        border-bottom: 1px solid #e5e7eb;
        text-align: left;
        font-size: 14px;
      }}
      th {{
        font-size: 12px;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: #52606d;
      }}
      .subtle {{
        color: #52606d;
      }}
      svg {{
        width: 100%;
        height: auto;
        display: block;
      }}
    </style>
  </head>
  <body>
    <main>
      <section class="card">
        <h1>{html.escape(summary['app_name'])} Report</h1>
        <p class="subtle">Trial: {html.escape(summary['trial_name'])}</p>
        <p class="subtle">Participant: {participant_name} | Institution: {participant_institution}</p>
        <p class="subtle">Email: {participant_email}</p>
        <p class="subtle">Start: {html.escape(summary['start_time'])} | Duration: {summary['elapsed_seconds']:.1f}s</p>
      </section>
      <section class="card">
        <h2>Summary</h2>
        <div class="grid">
          <div class="metric"><div class="metric-label">Average Reaction Time</div><div class="metric-value">{summary['average_reaction_time']:.2f}s</div></div>
          <div class="metric"><div class="metric-label">Successful Cues</div><div class="metric-value">{summary['successful_cues']}</div></div>
          <div class="metric"><div class="metric-label">Timed Out Cues</div><div class="metric-value">{summary['timed_out_cues']}</div></div>
          <div class="metric"><div class="metric-label">Completed Cues</div><div class="metric-value">{summary['completed_cues']}</div></div>
          <div class="metric"><div class="metric-label">Best Success Time</div><div class="metric-value">{summary['best_reaction_time']:.2f}s</div></div>
          <div class="metric"><div class="metric-label">Average Success Time</div><div class="metric-value">{summary['average_success_time']:.2f}s</div></div>
        </div>
      </section>
      <section class="card">
        <h2>Inside vs Outside</h2>
        {direction_comparison_html}
      </section>
      <section class="card">
        <h2>Reaction Time by Cue</h2>
        {chart_svg}
      </section>
      <section class="card">
        <h2>Cue Log</h2>
        <table>
          <thead>
            <tr>
              <th>Cue</th>
              <th>Outcome</th>
              <th>Target</th>
              <th>Reaction Time</th>
              <th>Target Angle</th>
              <th>Measured Angle</th>
            </tr>
          </thead>
          <tbody>
            {rows_html}
          </tbody>
        </table>
      </section>
    </main>
  </body>
</html>
"""

    def _build_chart_svg(self, rows):
        if not rows:
            return "<p>No chart data available.</p>"

        width = 920
        height = 390
        left_padding = 60
        right_padding = 24
        top_padding = 30
        bottom_padding = 114
        plot_width = width - left_padding - right_padding
        plot_height = height - top_padding - bottom_padding
        max_reaction_time = max(row["reaction_time"] for row in rows)
        if math.isclose(max_reaction_time, 0.0):
            max_reaction_time = 1
        else:
            max_reaction_time = int(math.ceil(max_reaction_time))

        bar_gap = 12
        bar_width = max(24.0, (plot_width - bar_gap * (len(rows) - 1)) / max(len(rows), 1))
        if len(rows) * bar_width + (len(rows) - 1) * bar_gap > plot_width:
            bar_width = (plot_width - bar_gap * (len(rows) - 1)) / len(rows)

        bars = []
        labels = []
        for index, row in enumerate(rows):
            bar_x = left_padding + index * (bar_width + bar_gap)
            bar_height = (row["reaction_time"] / max_reaction_time) * plot_height
            bar_y = top_padding + (plot_height - bar_height)
            fill = "#0f766e" if row["target_label"] == "inside_wrist" else "#2563eb"
            bars.append(
                f"<rect x='{bar_x:.2f}' y='{bar_y:.2f}' width='{bar_width:.2f}' "
                f"height='{bar_height:.2f}' rx='8' fill='{fill}' fill-opacity='0.82' "
                f"stroke='{fill}' stroke-width='1.5'></rect>"
            )
            bars.append(
                f"<text x='{bar_x + bar_width / 2:.2f}' y='{bar_y - 8:.2f}' "
                "text-anchor='middle' fill='#334e68' font-size='13'>"
                f"{row['reaction_time']:.2f}s</text>"
            )

            label_direction = "Inside" if row["target_label"] == "inside_wrist" else "Outside"
            labels.append(
                f"<text x='{bar_x + bar_width / 2:.2f}' y='{height - 62:.2f}' "
                "text-anchor='middle' fill='#334e68' font-size='12'>"
                f"Cue {row['cue_number']}</text>"
            )
            labels.append(
                f"<text x='{bar_x + bar_width / 2:.2f}' y='{height - 40:.2f}' "
                "text-anchor='middle' fill='#52606d' font-size='12'>"
                f"{label_direction}</text>"
            )

        y_axis_lines = []
        y_tick_values = [index / 2.0 for index in range(0, int(max_reaction_time * 2) + 1)]
        for value in y_tick_values:
            y = top_padding + plot_height - (value / max_reaction_time) * plot_height
            label = f"{int(value)}" if float(value).is_integer() else f"{value:.1f}"
            y_axis_lines.append(
                f"<line x1='{left_padding:.2f}' y1='{y:.2f}' x2='{width - right_padding:.2f}' y2='{y:.2f}' "
                "stroke='#e5e7eb' stroke-width='1'></line>"
            )
            y_axis_lines.append(
                f"<text x='{left_padding - 10:.2f}' y='{y + 4:.2f}' text-anchor='end' "
                "fill='#52606d' font-size='12'>"
                f"{label}</text>"
            )

        return f"""
<svg viewBox="0 0 {width} {height}" role="img" aria-label="Reaction time bar chart by cue and direction">
  <rect x="0" y="0" width="{width}" height="{height}" fill="#f8fafc" rx="12"></rect>
  {''.join(y_axis_lines)}
        <line x1="{left_padding:.2f}" y1="{top_padding:.2f}" x2="{left_padding:.2f}" y2="{top_padding + plot_height:.2f}" stroke="#94a3b8" stroke-width="1.5"></line>
        <line x1="{left_padding:.2f}" y1="{top_padding + plot_height:.2f}" x2="{width - right_padding:.2f}" y2="{top_padding + plot_height:.2f}" stroke="#94a3b8" stroke-width="1.5"></line>
        {''.join(bars)}
        {''.join(labels)}
        <text x="{left_padding + 20 :.2f}" y="15" fill="#0f766e" font-size="14">Inside Wrist</text>
  <text x="{left_padding + 150:.2f}" y="15" fill="#2563eb" font-size="14">Outside Wrist</text>
  <text x="{left_padding - 36:.2f}" y="{top_padding - 10 :.2f}" fill="#52606d" font-size="12">Seconds</text>
</svg>
"""

    def _save_chart_image(self, rows):
        if not rows:
            return

        os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
        os.makedirs(os.environ["MPLCONFIGDIR"], exist_ok=True)

        import matplotlib

        matplotlib.use("Agg")
        import matplotlib.pyplot as plt

        figure, axis = plt.subplots(figsize=(12, 5.5), dpi=160)
        x_positions = list(range(len(rows)))
        reaction_times = [row["reaction_time"] for row in rows]
        max_reaction_time = max(reaction_times)
        if math.isclose(max_reaction_time, 0.0):
            max_reaction_time = 1
        else:
            max_reaction_time = int(math.ceil(max_reaction_time))
        colors = [
            "#0f766e" if row["target_label"] == "inside_wrist" else "#2563eb"
            for row in rows
        ]

        bars = axis.bar(
            x_positions,
            reaction_times,
            color=colors,
            edgecolor=colors,
            linewidth=1.0,
            alpha=0.85,
        )

        for index, (bar, row) in enumerate(zip(bars, rows)):
            axis.text(
                bar.get_x() + bar.get_width() / 2.0,
                bar.get_height() + max_reaction_time * 0.02,
                f"{row['reaction_time']:.2f}s",
                ha="center",
                va="bottom",
                fontsize=9,
                color="#334e68",
            )

        axis.set_xticks(x_positions)
        axis.set_xticklabels(
            [
                f"Cue {row['cue_number']}\n"
                f"{'Inside' if row['target_label'] == 'inside_wrist' else 'Outside'}"
                for row in rows
            ],
            fontsize=9,
        )
        axis.tick_params(axis="x", pad=10)
        y_ticks = [index / 2.0 for index in range(0, int(max_reaction_time * 2) + 1)]
        axis.set_yticks(y_ticks)
        axis.set_yticklabels(
            [f"{int(value)}" if float(value).is_integer() else f"{value:.1f}" for value in y_ticks],
            fontsize=9,
        )
        axis.set_ylabel("Reaction Time (s)")
        axis.set_title("Reaction Time by Cue")
        axis.grid(axis="y", color="#e5e7eb", linewidth=1)
        axis.set_axisbelow(True)
        axis.spines["top"].set_visible(False)
        axis.spines["right"].set_visible(False)
        axis.set_ylim(0, max_reaction_time)
        figure.tight_layout()
        figure.subplots_adjust(bottom=0.32)
        figure.savefig(self.chart_image_path, format="png")
        plt.close(figure)

    def _render_row(self, row):
        return (
            "<tr>"
            f"<td>{row['cue_number']}</td>"
            f"<td>{html.escape(row['outcome'])}</td>"
            f"<td>{html.escape(row['target_label'])}</td>"
            f"<td>{row['reaction_time']:.2f}s</td>"
            f"<td>{row['target_angle']:+.1f}deg</td>"
            f"<td>{row['measured_angle']:+.1f}deg</td>"
            "</tr>"
        )

    def _format_start_time(self, start_time):
        if not start_time:
            return "Unknown"
        try:
            return dt.datetime.strptime(start_time, "%Y-%m-%d-%H-%M-%S").isoformat(sep=" ")
        except ValueError:
            return start_time

    def _get_value(self, mapping, key, default):
        value = mapping.get(key, default)
        if isinstance(value, list):
            return value[0] if value else default
        return value

    def _target_label(self, target_code):
        if int(target_code) == 1:
            return "inside_wrist"
        if int(target_code) == 2:
            return "outside_wrist"
        return "unknown"
